#import <QuartzCore/QuartzCore.h>
#import "ALTimelineElement.h"
#import "ALTimelineCanvas.h"


#pragma mark Constants
const CGFloat ALTimelineElementResizeAreaThickness = 7.0f;
const CGFloat ALTimelineElementFrameAnimationDuration = 0.25f;


#pragma mark -
#pragma mark Encoding keys
static NSString* const ALTimelineElementParentTimelineKey = @"ALTimelineElementParentTimelineKey";
static NSString* const ALTimelineElementResizableKey = @"ALTimelineElementResizableKey";
static NSString* const ALTimelineTrackingDirectionKey = @"ALTimelineTrackingDirectionKey";

#pragma mark Other keys
static NSString* const ALTimelineElementFrameOriginKey = @"frameOrigin";


#pragma mark -
#pragma mark Class Globals


#pragma mark -
@interface ALTimelineElement ()
@property (nonatomic, assign) BOOL viewIsAnimating;
@end


#pragma mark -
@interface ALTimelineElement (ALTimelineElementPrivateMethods)
-(void)copyViewFromElement_:(ALTimelineElement *)sourceElement toElement:(ALTimelineElement *)targetElement;
-(NSView *)copyOfView_:(NSView *)originalView;
-(void)updateConnectionsWithOriginalView_:(id)originalView originalElement:(ALTimelineElement *)originalElement destinationView:(id)destinationView destinationElement:(ALTimelineElement *)destinationElement;
-(void)addTrackingAreaWithDirection:(ALResizeDirection)direction;
-(NSRect)bottomTrackingRectForElementRect_:(NSRect)elementRect;
-(NSRect)topTrackingRectForElementRect_:(NSRect)elementRect;
-(NSRect)rightTrackingRectForElementRect_:(NSRect)elementRect;
@end


#pragma mark -
@implementation ALTimelineElement

@synthesize timeline;
@synthesize previousPosition;
@synthesize currentPosition;
@synthesize daysFromFirst;
@synthesize selected;
@synthesize resizable;
@synthesize viewIsAnimating;


#pragma mark -
#pragma mark NSCoding support

-(id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
	if (!(self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]))
		return nil;
	[self setResizable:YES];
	return self;
}

-(id)initWithCoder:(NSCoder *)aDecoder {
	if (!(self = [super initWithCoder:aDecoder]))
		return nil;
	[self setTimeline:[aDecoder decodeObjectForKey:ALTimelineElementParentTimelineKey]];
	if ([aDecoder containsValueForKey:ALTimelineElementResizableKey])
		[self setResizable:[aDecoder decodeBoolForKey:ALTimelineElementResizableKey]];
	else 
		[self setResizable:YES];
	return self;
}

-(void)encodeWithCoder:(NSCoder *)aCoder {
	[super encodeWithCoder:aCoder];
	[aCoder encodeConditionalObject:[self timeline] forKey:ALTimelineElementParentTimelineKey];
	[aCoder encodeBool:[self resizable] forKey:ALTimelineElementResizableKey];
}


#pragma mark -
#pragma mark NSCopying support

-(id)copyWithZone:(NSZone *)zone {
	ALTimelineElement *elementCopy = [[[self class] allocWithZone:zone] initWithNibName:[self nibName] bundle:[self nibBundle]];
	if (![self nibName])
		[self copyViewFromElement_:self toElement:elementCopy];
	[elementCopy setTimeline:[self timeline]];
	[elementCopy setPreviousPosition:[self previousPosition]];
	[elementCopy setCurrentPosition:[self currentPosition]];
	[elementCopy setDaysFromFirst:[self daysFromFirst]];
	return elementCopy;
}

-(void)copyViewFromElement_:(ALTimelineElement *)sourceElement toElement:(ALTimelineElement *)targetElement {
	NSView *sourceView = [sourceElement view];
	NSView *targetView = [self copyOfView_:sourceView];
	[self updateConnectionsWithOriginalView_:sourceView originalElement:sourceElement destinationView:targetView destinationElement:targetElement];
	[targetElement setView:[targetView autorelease]];
}

-(NSView *)copyOfView_:(NSView *)originalView {
	NSData *tempData = [NSKeyedArchiver archivedDataWithRootObject:originalView];
	return [[NSKeyedUnarchiver unarchiveObjectWithData:tempData] retain];
}

-(void)updateConnectionsWithOriginalView_:(id)originalView originalElement:(ALTimelineElement *)originalElement destinationView:(id)destinationView destinationElement:(ALTimelineElement *)destinationElement {
	if ([originalView respondsToSelector:@selector(setTarget:)] && [destinationView target] == nil) {
		id fromTarget = [originalView target];
		if (originalElement && fromTarget == originalElement)
			fromTarget = destinationElement;
		[destinationView setTarget:fromTarget];
	}
	if ([originalView respondsToSelector:@selector(setAction:)] && [destinationView action] == nil) 
		[destinationView setAction:[originalView action]];
	if ([originalView respondsToSelector:@selector(setDelegate:)] && [destinationView delegate] == nil) {
		id fromDelegate = [originalView delegate];
		if (originalElement && fromDelegate == originalElement)
			fromDelegate = destinationElement;
		[destinationView setDelegate:fromDelegate];
	}
	for (NSString *binding in [originalView exposedBindings]) {
		NSDictionary *bindingInfo = [originalView infoForBinding:binding];
		if (!bindingInfo) 
			continue;
		id observedObject = [bindingInfo valueForKey:NSObservedObjectKey];
		if (originalElement && observedObject == originalElement)
			observedObject = destinationElement;
		NSString *bindingKeyPath = [bindingInfo valueForKey:NSObservedKeyPathKey];
		[destinationView unbind:bindingKeyPath];
		[destinationView bind:binding toObject:observedObject withKeyPath:bindingKeyPath options:[bindingInfo valueForKey:NSOptionsKey]];
	}
	NSArray *fromSubviews = [originalView subviews];
	NSArray *toSubviews = [destinationView subviews];
	// TODO: PROVIDE ORDERING FUNCTION AND SORT SUBVIEWS BEFORE COMPARING THEM TO ENSURE WE ARE COMPARING APPLES TO APPLES
	for (NSUInteger viewIndex = 0; viewIndex < [fromSubviews count]; viewIndex++) {
		NSView *fromSubview = [fromSubviews objectAtIndex:viewIndex];
		NSView *toSubview = [toSubviews objectAtIndex:viewIndex];
		[self updateConnectionsWithOriginalView_:fromSubview originalElement:originalElement destinationView:toSubview destinationElement:destinationElement];
	}
}


#pragma mark -
#pragma mark Accessors

-(NSDate *)date {
	NSDate *delegateDate = [[[self timeline] delegate] timeline:[self timeline] dateForRepresentedObject:[self representedObject]];
	if (delegateDate)
		return delegateDate;
	if (![[self timeline] dateKeyPath])
		return nil;
	return [[self representedObject] valueForKeyPath:[[self timeline] dateKeyPath]];
}

-(NSDate *)dateForRepresentedObject_:(id)object {
    return nil;
}


-(NSSize)preferredSize {
	return [[self view] bounds].size;
}

-(NSSize)minimumSize {
	return NSZeroSize;
}

-(NSSize)maximumSize {
	return NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX);
}

-(void)setView:(NSView *)aView {
	[super setView:aView];
	[[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:nil];
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(viewFrameDidChange:) name:NSViewFrameDidChangeNotification object:aView];
	CABasicAnimation *frameAnimation = [CABasicAnimation animation];
	[frameAnimation setDuration:ALTimelineElementFrameAnimationDuration];
	[frameAnimation setDelegate:self];
	[frameAnimation setRemovedOnCompletion:NO];
	[frameAnimation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
	NSDictionary *animations = [NSDictionary dictionaryWithObjectsAndKeys:frameAnimation, ALTimelineElementFrameOriginKey, nil];
	[aView setAnimations:animations];
}

-(void)animationDidStart:(CAAnimation *)animation {
	[self setViewIsAnimating:YES];
}

-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
	[self setViewIsAnimating:NO];
	if (![[[self timeline] canvas] isElementVisible:self])
		[[self view] removeFromSuperview];
	else 
		[self updateResizeTrackingAreas];
}


#pragma mark -
#pragma mark Notifications

// TODO: THIS ISN'T A UNIVERSAL ANSWER - IF THE FRAME SHOULD HAPPEN TO COINCIDE WITH THE CURRENT POSITION, TRACKING AREAS AND VISIBILITY STATUS WOULD BE UPDATED WHILE THE ELEMENT IS STILL ANIMATING (LIKE IF ANIMATING AN ADD/REMOVE).  NEED TO USE CUSTOM ANIMATION INSTEAD
-(void)viewFrameDidChange:(NSNotification *)note {
	if ([self viewIsAnimating])
		return;
//	NSRect currentFrame = [[self view] frame];
	[self updateResizeTrackingAreas];
}


#pragma mark -
#pragma mark Setup and teardown

-(void)timelineElementWasAddedToTimeline {
	// Stub for subclasses to override
}

-(void)timelineElementWasRemovedFromTimeline:(ALTimelineView *)aTimeline {
	id timelineDelegate = [aTimeline delegate];
	if (![timelineDelegate respondsToSelector:@selector(timeline:uniqueStringForRepresentedObject:)])
		return;
	NSString *uniqueString = [timelineDelegate timeline:aTimeline uniqueStringForRepresentedObject:[self representedObject]];
	if (!uniqueString)
		return;
	[[NSUserDefaults standardUserDefaults] removeObjectForKey:uniqueString];
}


#pragma mark -
#pragma mark Editing

-(void)timelineElementDidBeginEditingAtPoint:(NSPoint)aPoint {
	
}

-(void)timelineElementDidEndEditing {
	
}


#pragma mark -
#pragma mark Resizing

-(void)updateResizeTrackingAreas {
	for (NSTrackingArea *trackingArea in [[self view] trackingAreas]) {
		if ([trackingArea owner] == self)
			[[self view] removeTrackingArea:trackingArea];
	}
	
	NSSize currentSize = [[self view] frame].size;
	NSSize maxSize = [self maximumSize];
	NSSize minSize = [self minimumSize];
	BOOL verticalSizable = currentSize.height < maxSize.height || currentSize.height > minSize.height;
	BOOL horizontalSizable = currentSize.width < maxSize.width || currentSize.width > minSize.width;
	if (verticalSizable)  {
		[self addTrackingAreaWithDirection:ALResizeDirectionDown];
		[self addTrackingAreaWithDirection:ALResizeDirectionUp];
	}
	if (horizontalSizable) 
		[self addTrackingAreaWithDirection:ALResizeDirectionRight];
}

-(ALResizeDirection)mouseInResizeTrackingArea:(NSPoint)mousePoint {
	NSRect elementRect = [[self view] bounds];
	NSRect bottomRect = [self bottomTrackingRectForElementRect_:elementRect];
	if ([[self view] mouse:mousePoint inRect:bottomRect])
		return ALResizeDirectionDown;
	NSRect topRect = [self topTrackingRectForElementRect_:elementRect];
	if ([[self view] mouse:mousePoint inRect:topRect])
		return ALResizeDirectionUp;
	NSRect rightRect = [self rightTrackingRectForElementRect_:elementRect];
	if ([[self view] mouse:mousePoint inRect:rightRect])
		return ALResizeDirectionRight;
	return ALResizeDirectionNone;
}

-(NSRect)bottomTrackingRectForElementRect_:(NSRect)elementRect {
	return NSMakeRect(ALTimelineElementResizeAreaThickness, 0, elementRect.size.width - (2 * ALTimelineElementResizeAreaThickness), ALTimelineElementResizeAreaThickness);
}

-(NSRect)topTrackingRectForElementRect_:(NSRect)elementRect {
	return NSMakeRect(ALTimelineElementResizeAreaThickness, elementRect.size.height - ALTimelineElementResizeAreaThickness, elementRect.size.width - (2 * ALTimelineElementResizeAreaThickness), ALTimelineElementResizeAreaThickness);
}

-(NSRect)rightTrackingRectForElementRect_:(NSRect)elementRect {
	return NSMakeRect(elementRect.size.width - ALTimelineElementResizeAreaThickness, ALTimelineElementResizeAreaThickness, ALTimelineElementResizeAreaThickness, elementRect.size.height - (2 * ALTimelineElementResizeAreaThickness));
}

-(void)addTrackingAreaWithDirection:(ALResizeDirection)direction {
	NSRect elementRect = [[self view] frame];
	NSRect trackingRect = NSZeroRect;
	switch (direction) {
		case ALResizeDirectionUp:
			trackingRect = [self topTrackingRectForElementRect_:elementRect];
			break;
		case ALResizeDirectionDown:
			trackingRect = [self bottomTrackingRectForElementRect_:elementRect];
			break;
		case ALResizeDirectionRight:
			trackingRect = [self rightTrackingRectForElementRect_:elementRect];
			break;
		default:
			return;
	}
	NSDictionary *userData = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInteger:direction], ALTimelineTrackingDirectionKey, nil];
	NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:trackingRect options:NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow owner:self userInfo:userData];
	[[self view] addTrackingArea:trackingArea];
}

-(void)mouseEntered:(NSEvent *)theEvent {
	NSDictionary *userData = [theEvent userData];
	ALResizeDirection direction = (ALResizeDirection)[[userData valueForKey:ALTimelineTrackingDirectionKey] integerValue];
	[[self cursorForResizeDirection:direction] set];
}

-(void)mouseExited:(NSEvent *)theEvent {
	[[NSCursor arrowCursor] set];
}

-(void)trackResizeWithEvent:(NSEvent *)mouseDownEvent withinRect:(NSRect)aRect {
	NSPoint elementMouseDownPoint = [[self view] convertPoint:[mouseDownEvent locationInWindow] fromView:nil];
	ALResizeDirection direction = [self mouseInResizeTrackingArea:elementMouseDownPoint];
	if (direction == ALResizeDirectionNone)
		return;
	NSView *superview = [[self view] superview];
	[[self view] removeFromSuperview];
	[superview addSubview:[self view] positioned:NSWindowAbove relativeTo:nil];
	NSSize maxSize = [self maximumSize];
	NSSize minSize = [self minimumSize];
	NSPoint mouseDownPoint = [superview convertPoint:[mouseDownEvent locationInWindow] fromView:nil]; 
	NSRect originalFrame = [[self view] frame];
	NSTimer *autoScrollTimer = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(autoScrollTimerDidFire_:) userInfo:nil repeats:YES];
	[[NSRunLoop currentRunLoop] addTimer:autoScrollTimer forMode:NSEventTrackingRunLoopMode];
	NSEvent *currentEvent = [[[self view] window] nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask];
	while ([currentEvent type] != NSLeftMouseUp) {
		NSRect newFrame = originalFrame;
		NSPoint currentMousePoint = [superview convertPoint:[currentEvent locationInWindow] fromView:nil];
		switch (direction) {
			case ALResizeDirectionUp:
				newFrame.size.height += currentMousePoint.y - mouseDownPoint.y;
				break;
			case ALResizeDirectionDown:
				newFrame.size.height += mouseDownPoint.y - currentMousePoint.y;
				break;
			case ALResizeDirectionRight:
				newFrame.size.width += currentMousePoint.x - mouseDownPoint.x;
				break;
			default:
				[autoScrollTimer invalidate];
				return;
		}
		newFrame.size.height = MIN(newFrame.size.height, maxSize.height);
		newFrame.size.height = MAX(newFrame.size.height, minSize.height);
		newFrame.size.width = MIN(newFrame.size.width, maxSize.width);
		newFrame.size.width = MAX(newFrame.size.width, minSize.width);
		if (direction == ALResizeDirectionDown)
			newFrame.origin.y += originalFrame.size.height - newFrame.size.height;
		newFrame = NSIntersectionRect(newFrame, aRect);
		[[self view] setFrame:newFrame];
		[[self cursorForResizeDirection:direction] set];
			
		currentEvent = [NSApp nextEventMatchingMask:NSLeftMouseUpMask | NSLeftMouseDraggedMask untilDate:[NSDate distantFuture] inMode:NSEventTrackingRunLoopMode dequeue:YES];
	}
	[autoScrollTimer invalidate];
	NSRect finalFrame = [[self view] frame];
	[self setPreviousPosition:[self currentPosition]];
	[self setCurrentPosition:finalFrame];
	id delegate = [[self timeline] delegate];
	if (![delegate respondsToSelector:@selector(timeline:uniqueStringForRepresentedObject:)])
		return;
	NSString *objectKey = [delegate timeline:[self timeline] uniqueStringForRepresentedObject:[self representedObject]];
	if (!objectKey)
		return;
	[[NSUserDefaults standardUserDefaults] setObject:NSStringFromSize(finalFrame.size) forKey:objectKey];
}

-(void)autoScrollTimerDidFire_:(NSTimer *)timer {
	NSEvent *currentEvent = [NSApp currentEvent];
	if ([currentEvent type] == NSLeftMouseDragged)
		[[self timeline] autoscroll:currentEvent];
}

-(NSCursor *)cursorForResizeDirection:(ALResizeDirection)direction {
	NSRect current = [[self view] frame];
	NSSize min = [self minimumSize];
	NSSize max = [self maximumSize];
	ALTimelineCanvas *canvas = [[self timeline] canvas];
	NSRect timelineRect = [canvas useableRectForBounds:[canvas bounds]];
	NSCursor *resizeCursor = nil;
	switch (direction) {
		case ALResizeDirectionUp:
			if (NSMaxY(current) >= NSMaxY(timelineRect) || current.size.height >= max.height)
				resizeCursor = [NSCursor resizeDownCursor];
			else if (current.size.height <= min.height)
				resizeCursor = [NSCursor resizeUpCursor];
			else 
				resizeCursor = [NSCursor resizeUpDownCursor];
			break;
		case ALResizeDirectionDown:
			if (current.origin.y <= timelineRect.origin.y || current.size.height >= max.height)
				resizeCursor = [NSCursor resizeUpCursor];
			else if (current.size.height <= min.height)
				resizeCursor = [NSCursor resizeDownCursor];
			else 
				resizeCursor = [NSCursor resizeUpDownCursor];
			break;
		case ALResizeDirectionRight:
			if (current.size.width <= min.width)
				resizeCursor = [NSCursor resizeRightCursor];
			else if (current.size.width >= max.width)
				resizeCursor = [NSCursor resizeLeftCursor];
			else 
				resizeCursor = [NSCursor resizeLeftRightCursor];
			break;
		default:
			resizeCursor = [NSCursor arrowCursor];
			break;
	}
	return resizeCursor;
}

@end
